Introduction

This document will provide an introduction to Bluetooth and some information about the Bluetooth stack on linux and some tools that are useful.

Overview

Bluetooth classic

History

Developed in the late 90’s and 1998 the Bluetooth Special Interest Group (Bluetooth SIG) was formed among a consortium of companies to push the standard. Bluetooth v2.0 solved a lot of issues and upped the maximum throughput to 3Mbps. In v2.1 one level of security were introduced by means of pairing devices. Bluetooth v3.0 introduced a way for paired devices to send data in higher speed via another medium (wifi or phy), this never got traction and is very seldom used. In Bluetooth v4.0 the Bluetooth low energy (Bluetooth LE) was introduced and now there were two different way of sending radio signals (devices could have one or the other or support for both techniques, dual mode).

Bluetooth classic is sometimes referred to as BR/EDR which is the modulation schemes it supports.

Bluetooth classic is mainly used for audio streaming.

Technique

Bluetooth classic uses the 2.4GHz ISM band. The band extends from 2400MHz to 2483.5MHz. This band is divided into 78 channels by Bluetooth classic, spaced 1MHz apart from 2402 to 2480MHz.

By using an adaptive frequency hopping algorithm, the goal is to avoid interference from other devices using the same spectra as baby monitors, wi-fi and microwave ovens.

Protocol stack

Protocol stack in blocks

Classic profiles

There is a large set of Bluetooth profiles, the most common are.

  • A2DP - Advanced Audio Distribution Profile is a profile that allows streaming audio from a source to a sink. For example, when you stream music from your iPhone to your car, this is done using the A2DP profile.

  • HFP - Hands-Free Profile used in Bluetooth headsets

  • SPP - Serial Port Profile emulates serial ports over Bluetooth to provide a simple substitute for existing RS-232

  • PBAP - Phone Book Access Profile allows access to a phone’s Phone Book, for example to display in a car to allow dialing

  • MAP - Message Access Profile allows exchange of messages between devices, for example to read SMS from a phone

  • AVRCP - A/V Remote Control Profile allows a Bluetooth device to act as a remote control, for example controlling video playback

  • PAN – Personal Area Network Profile used for sharing networking

Bluetooth Low Energy (Bluetooth LE)

History

Sometimes called Bluetooth Smart was introduced in Bluetooth 4.0.

It started in a Nokia project (2001) called Wibree before being adopted by the Bluetooth SIG.

Bluetooth LE Protocoll stack

Bluetooth LE protocoll stack

GAP

The Generic Access Profile (GAP) controls connections and advertising in Bluetooth. GAP enables the visibility of devices and determines how devices can interact with each other.

A device is either a peripheral or a central device. A peripheral device is often a small, low power resource constrained device that can connect to a much more powerful central device. Peripheral devices are typically things like sensors, proximity tags etc.

Central devices are more powerful, usually a pc, tablet or mobile phone.

Advertising and scan response data

A peripheral device can send 31 bytes of data in two ways. The advertising data and the scan response data. Only the advertising data is mandatory.

If the peripheral device supports scan response data the central device can send out a scan response request and the peripheral device will respond with the scan response data in which they can fit additional data that might be interesting.

During advertising a peripheral device can send data to many central devices.

When a central device connects to a peripheral one the advertising will stop but the two devices can then exchange data in both directions using GATT services and characteristics.

GATT

Generic ATTribute profile is used for sending data back and forth using services and characteristics. A generic protocol called Attribute protocol (ATT) stores Services, characteristics and related data in a lookup table using 16-bit IDs for each entry in the table.

A peripheral device can only be connected to one central device. A central device can be connected to multiple peripheral devices.

A GATT service is identified by the unique id which can be 16-bit or 128-bit. A service consists of one or more characteristics. For example, the Heart Rate Service 0x180D contains up to 3 characteristics which of the first one is mandatory, Heart Rate Measurement, Body Sensor Location and Heart Rate Control Point.

Each characteristic has its own 16-bit or 128-bit UUID. You can either write or read to a characteristic which has a single data point (could also be an array of related values such as x/y/z from an accelerometer).

Installation guide

Supported platforms

  • i.MX 8

    • CCpilot V1000

    • CCpilot V1200

Activate Bluetooth

Run the following commands in the terminal to enable Bluetooth.

sudo systemctl enable bluetooth-attach  
sudo systemctl enable bluetooth

You can check the status of the Bluetooth adapter by running the following command

bluetoothctl show

Setup pulseaudio for Bluetooth

If you need to connect Bluetooth speakers/headset to a v1x00 and perhaps have the ability to stream audio to a device you can do the following setup.

PulseAudio service file

Create a file called pulseaudio.service in the /etc/systemd/system/ folder. Use nano or similar and paste in the text below

[Unit]
Description=Pulseaudio sound server
After=avahi-daemon.service network.target

[Service]
ExecStart=/usr/bin/pulseaudio --system --log-target=syslog
ExecReload=/bin/kill -HUP \$MAINPID

[Install]
WantedBy=multi-user.target

After saving it, run the following command to enable the service.

sudo systemctl enable pulseaudio

Setup audio user

On v1x00 we need the following setup. Run as root.

groupadd pulse-access
usermod -G pulse-access -a root
usermod -G audio -a root
usermod -G pulse-access -a pulse
# User of printer group lp has access to bluetooth
usermod -G lp -a pulse

Setup pulseaudio system.pa for Bluetooth

The config file /etc/pulse/system.pa need the following two lines.

load-module module-bluetooth-policy
load-module module-bluetooth-discover

Add them last in the system.pa config file. Then reboot the device.

Test audio via bluetoothctl

From the command line you will run some commands as the ccs user.

If everything above is setup correctly you can start the bluetoothctl by running bluetoothctl

Type show to get status of the Bluetooth adapter. If it isn’t powered, use power on to power it on.

  1. Scan. Use the command scan on to start scanning for new devices

  2. Add headphones. Make sure your headphones are in pairing mode. Use the command devices to get a list of found devices. Localize your headphones, I have a pair of HSX headphones and they show up as Device FC:58:FA:F8:83:20 HSX 3.0 in the list. By using the mac-address of your device, pair with the headphones using the command pair FC:58:FA:F8:83:20. After pairing OK it is adviceable to trust the device. Use the command trust FC:58:FA:F8:83:20 to trust the device. Now you should be able to connect to it using the command connect FC:58:FA:F8:83:20.

  3. Add streaming device. Repeat the steps taken for the headphones but with your streaming device, usually a phone. Mine is called Device F8:E9:4E:A5:52:A1 Matss iPhone XS. Using that mac-address instead, repeat the above steps with pairing, trusting and connecting.

  4. Disable scanning with the command scan off

  5. Test it. For example, running spotify you can stream music to the v1x00 which will play it back on the speaker/headphones.

Trouble shooting

If you cannot stream music, first check that both the speaker/headphones and the streaming device are paired, trusted and connected.
Still in the bluetoothctl tool. Use the command info FC:58:FA:F8:83:20 but replace the mac-address with your devices mac addresses. For my headphones I get this info.

[HSX 3.0]# info FC:58:FA:F8:83:20
Device FC:58:FA:F8:83:20 (public)
        Name: HSX 3.0
        Alias: HSX 3.0
        Class: 0x00240418
        Icon: audio-card
        Paired: yes
        Trusted: yes
        Blocked: no
        Connected: yes
        LegacyPairing: no
        UUID: Serial Port               (00001101-0000-1000-8000-00805f9b34fb)
        UUID: Headset                   (00001108-0000-1000-8000-00805f9b34fb)
        UUID: Audio Sink                (0000110b-0000-1000-8000-00805f9b34fb)
        UUID: A/V Remote Control Target (0000110c-0000-1000-8000-00805f9b34fb)
        UUID: A/V Remote Control        (0000110e-0000-1000-8000-00805f9b34fb)
        UUID: Handsfree                 (0000111e-0000-1000-8000-00805f9b34fb)
        UUID: PnP Information           (00001200-0000-1000-8000-00805f9b34fb)
        Modalias: bluetooth:v000ApFFFFdFFFF

Above you also see what profiles the headphones support, stated with different UUID’s.

If the sound quality is bad make sure you are not in discovery mode. Run scan off

BlueZ

BlueZ is the official Bluetooth stack for Linux and we are using version 5.50. The recommended API to communicate with the BlueZ stack from the application is through D-Bus. It provides an API for handling the different profiles. The daemons which handle most of the API is through bluetoothd and obexd. For audio-related profiles you need pulseaudio (which is a sound server system used in modern linux distribution) . You need the oFono library for using the hands free profile (HFP) for managing phone calls. For more information on the Bluez API, pulseaudio and oFono see the chapter Further reading.

Bluez diagram

Above, the bluez protocol stack for Bluetooth classic

D-Bus

D-Bus is a message-oriented middleware for inter process communication on the same machine. It is divided into a system bus and a session bus (one for each user session). A message bus daemon will be running which acts like a router. Each application that uses D-Bus has a one to one connection with the message bus daemon. One instance of this message bus daemon runs the system bus, then you will have one instance per user session running the session bus (applications in the user session can communicate with one another). Applications register a unique service name to route messages from one application to another.

Example from Qt documentation

One app register a service that implements a RPN calculator, also the app provides an object that implements an interface.

The client app can then call methods on the object. Looks like this in qt.

QDBusInterface remoteApp( "com.example.Calculator", "/Calculator Operations","org.mathematics.RPNCalculator" );
remoteApp.call( "PushOperand", 2 );
remoteApp.call( "PushOperand", 2 );
remoteApp.call( "ExecuteOperation", "+" );
QDBusReply<int> reply = remoteApp.call( "PopOperand" );

if ( reply.isValid() )
	printf( "%d", reply.value() ); // prints 4

There are four types of messages being sent on the message bus.

  • Method call message

  • Method return

  • Error messages (return an exception caused by invoking a method)

  • Signal messages (notification that a signal has been emitted)

Important elements are

  • Services (collection of objects providing a specific feature)

  • Objects (are attached to a service, has a path like /cc/aux/backlight)

  • Interfaces (implemented by objects, has properties, methods and signals)

  • Clients (applications that uses a D-Bus service)

You usually need the following for invoking a method.

  • Address, where to find the daemon

  • Bus name, system bus or session bus

  • Object path, where to find the object

  • Interface, the type of the object

  • Method, the actual action to be performed

D-Bus interaction diagram

Figure showing how two applications are communicating via D-Bus.

dbus-monitor

For debugging purposes it can be useful to see the very messages being transmitted over D-Bus.
To monitor the system bus you can use the command line tool

sudo dbus-monitor --system

And similar for the session bus

dbus-monitor --session

Bluetooth code examples

In the following examples we use c++ with Qt and its D-Bus interface classes. You can of course use other programming languages.

D-Bus and datatypes

Sometimes Qt doesn’t know how to handle data coming via D-Bus. Using the Q_DECLARE_METATYPE macro with common types you can get around this problem.
Below is two commonly recurring datatypes which are good to declare.

#ifndef TYPEDEFS_H
#define TYPEDEFS_H

/* define and declare custom types used with bluez D-Bus interfaces */

#include <QObject>
#include <QDBusObjectPath>

typedef QMap<QString,QVariantMap> InterfacesMap;
typedef QMap<QDBusObjectPath,InterfacesMap> ObjectsMap;

Q_DECLARE_METATYPE(InterfacesMap)
Q_DECLARE_METATYPE(ObjectsMap)
#endif // TYPEDEFS_H

You also need to call the qDBusRegisterMetaType function

/* Register meta types for D-Bus */
qDBusRegisterMetaType<InterfacesMap>();
qDBusRegisterMetaType<ObjectsMap>();

Documentation The Qt D-Bus Type System

Important D-Bus services for BlueZ

org.freedesktop.DBus.ObjectManager

This interface defines signals InterfaceAdded and InterfaceRemoved
This signals are emitted when Bluez discovers new devices and when devices are no longer known to bluez.

The method GetManagedObjects() is also useful as you can get all of the objects that a D-bus connected process possesses. See example below Get list of devices

org.freedesktop.DBus.Properties

This interface is useful for setting and reading properties. It also has a signal called PropertiesChanged.
Bluez device objects implements this interface so you can easily read and write properties and also react to properties being changed.
Look up org.bluez.Device1. See Latest bluez api

Powering Bluetooth on and off

Having some defines variables for commonly used strings can be good practice like below.

const QString BLUEZ_DBUS_SERVICE = "org.bluez";
const QString BLUEZ_DBUS_PATH = "/org/bluez/hci0";
const QString BLUEZ_DBUS_IF = "org.bluez.Adapter1";

Using the above in code to power the Bluetooth adapter on and off by setting the property Powered to true or false. With QDBusInterface you can create a reference to a remote object. In this case it lives on the systemBus. You can check that the reference is valid by calling the isValid() function.

void BluetoothAdapter::powerOff() {
    QDBusInterface ifc(
        BLUEZ_DBUS_SERVICE,
        BLUEZ_DBUS_PATH,
        BLUEZ_DBUS_IF,
        QDBusConnection::systemBus());
    if (ifc.isValid()) {
        ifc.setProperty("Powered", false);
    }
}

Scanning for devices

The org.bluez.Adapter1 has methods for starting and stopping discovery (which is essential scanning).
Below you have two code examples of starting and stopping discovery.

void BluetoothAdapter::startDiscovery() {
    QDBusInterface adapter("org.bluez",
        "/org/bluez/hci0",
        "org.bluez.Adapter1",
        QDBusConnection::systemBus());
    QDBusReply<void> reply = adapter.call("StartDiscovery");
    if (!reply.isValid()) {
        qWarning() << Q_FUNC_INFO << "Failed to start discovery: " << reply.error().message();
    }
}
void BluetoothAdapter::stopDiscovery() {
    QDBusInterface adapter("org.bluez",
        "/org/bluez/hci0",
        "org.bluez.Adapter1",
        QDBusConnection::systemBus());
    QDBusReply<void> reply = adapter.call("StopDiscovery");
    if (!reply.isValid()) {
        qWarning() << Q_FUNC_INFO << "Failed to stop discovery: " << reply.error().message();
    }
}

Pair with device

To pair with a specific device you need to know its object path. If you are catching the InterfaceAdded like mentioned earlier you can keep a list of devices to pair with.

Your device will likely have another ID than mine dev_F8_E9_4E_A5_52_A1
It is a good idea to also set Trusted to true.

QDBusInterface *dev = new QDBusInterface("org.bluez",
            "/org/bluez/hci0/dev_F8_E9_4E_A5_52_A1",
            "org.bluez.Device1",
            QDBusConnection::systemBus());

    if (dev->property("Paired").toBool())
        return;
    else {
        dev->asyncCall("Pair");
        dev->setProperty("Trusted", true);
    }
}

How to catch changing properties

Below is an example of how to catch properties changing on any Bluetooth device known by bluez.

m_interface = new QDBusInterface("org.bluez",
    "/org/bluez/hci0",
    "org.bluez.Device1",
    QDBusConnection::systemBus());

if (!m_interface->connection().connect("org.bluez",
        "",
        "org.freedesktop.DBus.Properties",
        "PropertiesChanged",
        this,
        SLOT(propertiesChanged(const QString, const QMap<QString, QVariant>, const QStringList)))) {
    qWarning() << Q_FUNC_INFO << "Failed to connect to D-Bus properties changed signal" << m_interface->lastError().name();
}

Get a list of Bluetooth devices

Below an example of how to get all managed objects from the ObjectManager and pick out the interesting ones for Bluetooth.

You would problably want to add devices to some sort of list and for the adapter you might set powerOn. See code below.

QDBusInterface manager("org.bluez",
    "/",
    "org.freedesktop.DBus.ObjectManager",
    QDBusConnection::systemBus());

QDBusReply<QMap<QDBusObjectPath, QMap<QString, QVariantMap>>> reply;
reply = manager.call("GetManagedObjects");
if (!reply.isValid()) {
    qWarning() << Q_FUNC_INFO << "Failed to connect to bluez: " << reply.error().message();
    return;
}

auto objects = reply.value();

for (auto i = objects.begin(); i != objects.end(); ++i) {
    auto ifaces = i.value();
    for (auto j = ifaces.begin(); j != ifaces.end(); ++j) {
        if (j.key() == "org.bluez.Device1")
            addDevice(i.key());
        else if (j.key() == "org.bluez.Adapter1")
            powerOn();
    }
}

Control streaming of music

If a device supports the “Audio/Video Remove Control Profile Target” (“0000110c-0000-1000-8000-00805f9b34fb”) you can control the playback via the media-api Bluez media api

Bluez will add an object like below on D-Bus (here the device has the mac address 48:26:2C:AC:77:B9) /org/bluez/hci0/dev_48_26_2C_AC_77_B9/player0

Get the D-Bus interface with

m_objectPath = QDBusObjectPath("/org/bluez/hci0/dev_48_26_2C_AC_77_B9/player0");
p_mediaInterface = new QDBusInterface ("org.bluez",
                                       m_objectPath.path(),
                                       "org.bluez.MediaPlayer1",
                                       QDBusConnection::systemBus());

For keeping a record of what is playing etc. Connect the propertiesChanged signal.

if(!p_mediaInterface->connection().connect("org.bluez",
                                     m_objectPath.path(),
                                     "org.freedesktop.DBus.Properties",
                                     "PropertiesChanged",
                                     this,
                                     SLOT(propertiesChanged(const QString, const QMap<QString,QVariant>, const QStringList)))) {
    qDebug() << "Failed to connect to D-Bus properties changed signal" << p_mediaInterface->lastError().name();
}

When the above is setup correctly you can simply control the playback by calling.

p_mediaInterface->call("Play");
p_mediaInterface->call("Stop");
p_mediaInterface->call("Pause");
p_mediaInterface->call("Next");
p_mediaInterface->call("Previous");

When a property changes you can possible read status and what track is playing etc.

void BluetoothMediaPlayer::propertiesChanged(const QString path, const QMap<QString, QVariant> changedProperties, const QStringList invalidatedProperties)
{
    Q_UNUSED(invalidatedProperties)
    Q_UNUSED(path)

    QMapIterator<QString, QVariant> mapIter(changedProperties);
    while(mapIter.hasNext()) {
        mapIter.next();

        if(mapIter.key() == "Status") {
            setStatus(mapIter.value().toString());
        }

        if(mapIter.key() == "Track") {
            QMap<QString, QVariant> trackData = qdbus_cast<QMap<QString, QVariant> >(mapIter.value());
            if(!trackData.value("Artist").isNull()) setArtist(trackData.value("Artist").toString());
            if(!trackData.value("Title").isNull()) setTitle(trackData.value("Title").toString());
            if(!trackData.value("Album").isNull()) setAlbum(trackData.value("Album").toString());
            if(!trackData.value("TrackNumber").isNull()) setTrackNumber(trackData.value("TrackNumber").toInt());
            if(!trackData.value("NumberOfTracks").isNull()) setNumberOfTracks(trackData.value("NumberOfTracks").toInt());
        }
    }
}

Serial port profile as client

Using SPP is like having two devices connected by a serial cable.
The QtBluetooth way of finding nearby devices is used here but could be replaced with the D-Bus version.

Qt has a subset of Bluetooth-classes. In this example we will use the QBluetoothDeviceDiscoveryAgent class to find nearby devices that supports SPP.

Below we have created a Discovery class will keep track of nearby devices and has slots for useful signals coming from the QBluetoothDeviceDiscoveryAgent.

discovery.h

#ifndef DISCOVERY_H
#define DISCOVERY_H

#include <QObject>
#include <QBluetoothDeviceDiscoveryAgent>
#include <QBluetoothDeviceInfo>
#include <QBluetoothUuid>
#include <QBluetoothSocket>
#include "devicename.h"

class Discovery : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool scanning READ scanning NOTIFY scanningChanged)
    Q_PROPERTY(QVariant devices READ devices NOTIFY devicesChanged)
    Q_PROPERTY(QString info READ info NOTIFY infoChanged)
public:
    explicit Discovery(QObject *parent = nullptr);
    void setup();
    bool scanning() const;
    QString info() const;
    QVariant devices();
    Q_INVOKABLE void startSearch();
    Q_INVOKABLE void sendBytes(long noOfBytes);

private slots:
    void addDevice(const QBluetoothDeviceInfo &device);
    void scanError(QBluetoothDeviceDiscoveryAgent::Error error);
    void scanFinished();
    void connectedToService();

signals:
    void scanningChanged();
    void devicesChanged();
    void infoChanged();

private:     
     void setInfo(QString value);
     QBluetoothDeviceDiscoveryAgent *m_deviceDiscoveryAgent;
     QList<DeviceName*> m_devices;
     QBluetoothSocket *m_btSocket;
     QString m_info;
     bool m_busy;
};

#endif // DISCOVERY_H

By setting it up like this we can catch discovered devices. When the socket connects our function connectedToService will be called.

void Discovery::setup()
{
    m_deviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);

    connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &Discovery::addDevice);
    connect(m_deviceDiscoveryAgent, static_cast<void (QBluetoothDeviceDiscoveryAgent::*)(QBluetoothDeviceDiscoveryAgent::Error)>(&QBluetoothDeviceDiscoveryAgent::error),
            this, &Discovery::scanError);

    connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &Discovery::scanFinished);
    connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::canceled, this, &Discovery::scanFinished);

    m_btSocket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol, this);
    connect(m_btSocket, &QBluetoothSocket::connected, this, &Discovery::connectedToService);
}

To activate scanning you call

m_deviceDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethod::ClassicMethod);

The key is to find devices which supports the serial port profile. In the function addDevice we check the UUIDS for matching SerialPortProfile. In the code below we try to connect to the first found Bluetooth device which supports the serial port profile.

void Discovery::addDevice(const QBluetoothDeviceInfo &device)
{    
    m_devices.append(new DeviceName(device));
    QList<QBluetoothUuid> services = device.serviceUuids();
    QBluetoothUuid theId;

    foreach(theId, services)
    {
        if (theId == QBluetoothUuid::SerialPort && m_busy == false)
        {                
            qDebug() << "Found the unit!!! Connecting to " << device.name();
            setInfo(tr("Found device support Serial Port %1").arg(device.name()));
            m_btSocket->connectToService(device.address(), theId);
            m_busy = true;
        }
    }        
    emit devicesChanged();
}

When connected via the socket you can start to send and receive bytes. Like writing a message

char data[] = {"Hello SPP server!"};
m_btSocket->write(data);

Read more about QBluetoothSockets at QBluetoothSocket

Serial port profile as server

Using Bluez D-Bus API we can use the profile-api to register that we can handle the SPP as server. Using an adaptor class for the org.bluez.Profile1 interface is the easiest way.

Using the tool we can turn an XML description of the interface. Saved as Profile1.xml.

<interface name="org.bluez.Profile1">
        <method name='Release'>
        </method>
        <method name='NewConnection'>
            <arg type='o' name='device' direction='in'/>
            <arg type='h' name='fd' direction='in'/>
            <annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="InterfaceMap"/> 
            <arg type='a{sv}' name='fd_properties' direction='in'>
                <annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="InterfaceMap"/>
            </arg>	    
        </method>
        <method name='RequestDisconnection'>
        <arg type='o' name='device' direction='in'/>
        </method>
</interface>

The .h file

/*
 * This file was generated by qdbusxml2cpp version 0.8
 * Command line was: qdbusxml2cpp Profile1.xml -a bluezprofile1adaptor
 *
 * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
 *
 * This is an auto-generated file.
 * This file may have been hand-edited. Look for HAND-EDIT comments
 * before re-generating it.
 */

#ifndef BLUEZPROFILE1ADAPTOR_H
#define BLUEZPROFILE1ADAPTOR_H

#include <QtCore/QObject>
#include <QtDBus/QtDBus>
#include "typedefs.h"
QT_BEGIN_NAMESPACE
class QByteArray;
template<class T> class QList;
template<class Key, class Value> class QMap;
class QString;
class QStringList;
class QVariant;
QT_END_NAMESPACE

/*
 * Adaptor class for interface org.bluez.Profile1
 */
class Profile1Adaptor: public QDBusAbstractAdaptor
{
    Q_OBJECT
    Q_CLASSINFO("D-Bus Interface", "org.bluez.Profile1")
    Q_CLASSINFO("D-Bus Introspection", ""
"  <interface name=\"org.bluez.Profile1\">\n"
"    <method name=\"Release\"/>\n"
"    <method name=\"NewConnection\">\n"
"      <arg direction=\"in\" type=\"o\" name=\"device\"/>\n"
"      <arg direction=\"in\" type=\"h\" name=\"fd\"/>\n"
"      <annotation value=\"InterfaceMap\" name=\"org.qtproject.QtDBus.QtTypeName.In2\"/>\n"
"      <arg direction=\"in\" type=\"a\0x007Bsv\0x007D\" name=\"fd_properties\"/>\n"
"    </method>\n"
"    <method name=\"RequestDisconnection\">\n"
"      <arg direction=\"in\" type=\"o\" name=\"device\"/>\n"
"    </method>\n"
"  </interface>\n"
        "")
public:
    Profile1Adaptor(QObject *parent);
    virtual ~Profile1Adaptor();

public: // PROPERTIES
public Q_SLOTS: // METHODS
    void NewConnection(const QDBusObjectPath &device, const QDBusUnixFileDescriptor &fd, QVariantMap  fd_properties);
    void Release();
    void RequestDisconnection(const QDBusObjectPath &device);
Q_SIGNALS: // SIGNALS
};

#endif

The .cpp file

/*
 * This file was generated by qdbusxml2cpp version 0.8
 * Command line was: qdbusxml2cpp Profile1.xml -a bluezprofile1adaptor
 *
 * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
 *
 * This is an auto-generated file.
 * Do not edit! All changes made to it will be lost.
 */

#include "bluezprofile1adaptor.h"
#include <QtCore/QMetaObject>
#include <QtCore/QByteArray>
#include <QtCore/QList>
#include <QtCore/QMap>
#include <QtCore/QString>
#include <QtCore/QStringList>
#include <QtCore/QVariant>

/*
 * Implementation of adaptor class Profile1Adaptor
 */

Profile1Adaptor::Profile1Adaptor(QObject *parent)
    : QDBusAbstractAdaptor(parent)
{
    // constructor
    setAutoRelaySignals(true);
}

Profile1Adaptor::~Profile1Adaptor()
{
    // destructor
}

void Profile1Adaptor::NewConnection(const QDBusObjectPath &device, const QDBusUnixFileDescriptor &fd, QVariantMap fd_properties)
{
    qDebug() << __PRETTY_FUNCTION__ << device.path();
    // handle method call org.bluez.Profile1.NewConnection
    QMetaObject::invokeMethod(parent(), "NewConnection", Q_ARG(QDBusObjectPath, device), Q_ARG(QDBusUnixFileDescriptor, fd), Q_ARG(QVariantMap, fd_properties));
}

void Profile1Adaptor::Release()
{
    // handle method call org.bluez.Profile1.Release
    QMetaObject::invokeMethod(parent(), "Release");
}

void Profile1Adaptor::RequestDisconnection(const QDBusObjectPath &device)
{
    // handle method call org.bluez.Profile1.RequestDisconnection
    QMetaObject::invokeMethod(parent(), "RequestDisconnection", Q_ARG(QDBusObjectPath, device));
}

Using the above adaptor class it is pretty straight forward to implement the functions as in the code below.

#ifndef SPP_SERVER_H
#define SPP_SERVER_H

#include <QObject>
#include <QtDBus>
#include "typedefs.h"
#include <stdio.h>
#include <unistd.h>

class SPP_server : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString message READ message NOTIFY newData)
    Q_PROPERTY(QString info READ info NOTIFY newInfo)
public:
    explicit SPP_server(QObject *parent = nullptr);
    const QString message();
    const QString info();

    Q_INVOKABLE void sendMsg(QString value);

public slots:
    void NewConnection(const QDBusObjectPath &device, const QDBusUnixFileDescriptor &fd, QVariantMap fd_properties);
    void Release();
    void RequestDisconnection(const QDBusObjectPath &device);

signals:
    void newData();
    void newInfo();
private:
    void pollData();
    void setMessage(QString value);
    void setInfo(QString value);
    int m_fileDescriptor;
    QString m_message;
    long m_receivedBytes;
    QString m_info;
};

#endif // SPP_SERVER_H

and the .cpp part. From the api-description you can read about the properties being defined in args.

#include "SPP_Server.h"
#include <QDebug>
#include <QTimer>
#include <QDateTime>

SPP_server::SPP_server(QObject *parent) : QObject(parent)
{
    m_fileDescriptor = -1;
    m_receivedBytes = 0;

    QDBusInterface profileManager("org.bluez", "/org/bluez", "org.bluez.ProfileManager1", QDBusConnection::systemBus());

    qDebug() << "SPP_server profilemanager calls";
    if (profileManager.isValid()) {
        QVariant theProfilePath{QVariant::fromValue(QDBusObjectPath("/example/spp"))};

        // SerialPortServiceClass
        QString server_uuid = "00001101-0000-1000-8000-00805f9b34fb";

        QMap<QString, QVariant> args;
        args["AutoConnect"] = true;
        args["Role"] = "server";
        args["Name"] = "Our SPP";
        args["Channel"] = QVariant::fromValue((uint16_t)8);

        args["RequireAuthentication"] = false;
        args["RequireAutharization"] = false;

        // Call void RegisterProfile(object profile, string uuid, dict options) 
        QDBusMessage msg{profileManager.call("RegisterProfile", theProfilePath, server_uuid, args)};
        if (msg.type() != QDBusMessage::ErrorMessage) {
            qDebug() << "Registerprofile in SPP_server went well";
        }
        else
        {
            qDebug() << msg.errorMessage();
        }
    
    }
    else
    {
        qDebug() << profileManager.lastError().message();
    }

    QTimer *timer = new QTimer(this);
    connect(timer, &QTimer::timeout, this, &SPP_server::pollData);
    timer->start(50);
}

const QString SPP_server::message()
{
    return m_message;
}

void SPP_server::sendMsg(QString value)
{
    // Write to client if we have a filedescriptor
    if (m_fileDescriptor <= 0)
        return;
    QByteArray arr = value.toUtf8();

    write(m_fileDescriptor, arr.data(), arr.length());

}
void SPP_server::setMessage(QString value)
{
    if (value != m_message)
    {
        m_message = value;
        emit newData();
    }
}

const QString SPP_server::info()
{
    return m_info;
}
void SPP_server::setInfo(QString value)
{
    if (value != m_info)
    {
        m_info = value;
        emit newInfo();
    }
}

void SPP_server::NewConnection(const QDBusObjectPath &device, const QDBusUnixFileDescriptor &fd, QVariantMap fd_properties)
{
     qDebug() << __PRETTY_FUNCTION__ << device.path();

     if (fd.isValid())
     {
         // Take it or leave it?
         if (m_fileDescriptor != -1)
             close(m_fileDescriptor);

         m_fileDescriptor = dup(fd.fileDescriptor()); 

     }
}

void SPP_server::Release()
{
    // Clean up stuff
     qDebug() << __PRETTY_FUNCTION__;
}

void SPP_server::RequestDisconnection(const QDBusObjectPath &device)
{
    qDebug() << __PRETTY_FUNCTION__ << device.path();
    // Oh.. should disconnect here
    if (m_fileDescriptor > 0)
    {
        close(m_fileDescriptor);
        m_fileDescriptor = -1;
    }
}

void SPP_server::pollData()
{
   // Do we have a connection?
   if (m_fileDescriptor <= 0)
       return;

   // Read data, if small enough, show it UI
   char buf[65536];
   int readCount = read(m_fileDescriptor, buf, sizeof(buf));
   if (readCount > 0)
   {
       m_receivedBytes += readCount;
       // Are we receiveing something big? maybe not good to show in ui as text
       if (readCount < 400)
            setMessage(QString::fromUtf8(buf, readCount));

       // qDebug() << "Total received bytes : " << m_receivedBytes;
       setInfo(tr("Total received bytes : %1").arg(m_receivedBytes));
   }

   qDebug() << "TimeStamp: " << QDateTime::currentMSecsSinceEpoch() << " Total received bytes : " << m_receivedBytes;
}

You need to register the spp_server to D-Bus for bluez to route calls to it. in your main.cpp you need something like below.

SPP_server server;
Profile1Adaptor profadaptor(new Profile1Adaptor(&server));
QDBusConnection::systemBus().registerObject("/example/spp", &server);

Send file to device

Files can be sent to a device if it supports the OBEX Object Push Profile. From the UUIDs property of a device you can check if the OPP profile is among them “00001105-0000-1000-8000-00805f9b34fb”.

To send a file you can use QBluetoothTransferRequest. The receiving device might need some extra attributes about the file being transmitted. Added with the setAttribute fuction below.

void BluetoothTransfer::transferFile(QString address, QString fileName)
{
    qDebug() << __PRETTY_FUNCTION__ << address << fileName;

    QBluetoothAddress addr(address);
    QBluetoothTransferRequest request(addr);
    request.setAttribute(QBluetoothTransferRequest::DescriptionAttribute, QString("A simple txtfile"));
    request.setAttribute(QBluetoothTransferRequest::NameAttribute, QString("testfile.txt"));
    request.setAttribute(QBluetoothTransferRequest::TypeAttribute, "text/plain");
    QFile *file = new QFile(fileName);
    request.setAttribute(QBluetoothTransferRequest::LengthAttribute, QVariant(file->size()));
    request.setAttribute(QBluetoothTransferRequest::TimeAttribute, 1);
    qDebug() << "Size of file to send: " << QVariant(file->size());

    // Ask the transfer manager to send it
    QBluetoothTransferReply *reply = p_manager->put(request, file);
    if (reply->error() == QBluetoothTransferReply::NoError) {

        // Connect to the reply's signals to be informed about the status and do cleanups when done
        QObject::connect(reply, SIGNAL(finished(QBluetoothTransferReply*)),
                         this, SLOT(transferFinished(QBluetoothTransferReply*)));
        QObject::connect(reply, SIGNAL(error(QBluetoothTransferReply::TransferError)),
                         this, SLOT(transferError(QBluetoothTransferReply::TransferError)));
    } else {
        qWarning() << "Cannot push file" << reply->errorString();
    }
}

By using QObject::connect(…) to connect the finished signal and error signal of QBluetoothTransferReply you can keep track of the transfer status.

Read phonebook from mobile device

Similar to sending a file via the Object push profile, quering for phonebook contacts also uses the bluez obex (Object Exchange) under the hood.

The api can be viewed here Bluez Obex-api

Using the address of the device you want to read phonebook from (Address is a property of the device-api).
We create a new OBEX session where we target pbap (phonebook access). If allowed by the device we will get a session path in return.

void BluetoothContacts::getContacts(QString deviceAddress)
{
    QDBusInterface client("org.bluez.obex",
                          "/org/bluez/obex",
                          "org.bluez.obex.Client1",
                          QDBusConnection::sessionBus());

    QMap<QString, QVariant> args;
    args["Target"] = "pbap";
    QDBusReply<QDBusObjectPath> reply = client.call("CreateSession", deviceAddress, args);
    if (!reply.isValid()) {
        qWarning() << "Failed to create session: " << reply.error();
        return;
    }

    QString sessionPath = reply.value().path();
    qDebug() << "session path:" << sessionPath;

    listContacts(sessionPath);
}

In the listContacts function we create a QDBusInterface with the sessionPath as path argument.

void BluetoothContacts::listContacts(const QString &path)
{
    QDBusInterface client("org.bluez.obex",
                          path,
                          "org.bluez.obex.PhonebookAccess1",
                          QDBusConnection::sessionBus());

    QDBusReply<void> reply1 = client.call("Select", "int", "PB");
    if (!reply1.isValid()) {
        qWarning() << "Failed to select internal phonebook: " << reply1.error();
        return;
    }

Using the Select function (also described in the obex-api) we target the internal phonebook. Below we will then try to pull data from it but only the name and the phone number by providing FN and TEL for the filter.

    QMap<QString, QVariant> filterMap;
    QStringList filters;
    QStringList fields;
    filters << "FN" << "TEL";
    fields << "FN" << "TEL";    
    filterMap["Fields"] = fields;
    
    QDBusReply<QDBusObjectPath> reply = client.call("PullAll", "phonebook.vcf", filterMap);    
    if(!reply.isValid()) {
        qWarning() << "Failed to pull all contacts: " << reply.error();
        return;
    }

m_phonebookTransferPath = reply.value().path();

If the above went well we store the path to the transfer session of the phonebook data. By using a timer we can check the status of the ongoing transfer.

void BluetoothContacts::checkTransferStatus()
{    
        QDBusInterface sessionClient("org.bluez.obex",
                              m_phonebookTransferPath,
                              "org.bluez.obex.Transfer1",
                              QDBusConnection::sessionBus());

        QString status = sessionClient.property("Status").toString();
   
        // Eventually the status will be complete or empty "" when transfer is done
        if(status == QString("complete") || status == "") {
            qDebug() << "Emitting contactsTransferReady";
            emit contactsTransferReady();
        }
        else if(status == QString("error")) {
            qWarning() << "contacts transfer failed";
        }
        else {
            QTimer::singleShot(100, this, SLOT(checkTransferStatus()));            
        }
}

When the transfer is done you can access the phonebook via a QFile object.

QFile file(QDir::homePath() + "/phonebook.vcf");

Reading data from a Bluetooth LE sensor

Let’s assume a real sensor example, such as the BT510 sensor from Laird
Bt-sensor Sentrius BT510 Laird sensor user guide

This is a beacon like sensor that transmit data to anyone nearby. No connect function call needed. It does transmit data by utilizing the few bytes you can freely define in the 31 byte advertisement package for Bluetooth LE devices.

Looking into the guide of this sensor you get a grasp of how to interpret the data.

The key for using such a sensor is knowing that the data shows up in ManufacturerData property of the device. From the bluez D-Bus device-api

dict ManufacturerData [readonly, optional]
Manufacturer specific advertisement data.
Keys are 16 bits Manufacturer ID followed by its byte array value.

The sensor can send temperature, battery voltage, movement detection and open/close status of a switch. It also have some way of triggering alarms on different temperature levels. This is encoded into the advertisement data.

Using D-Bus connect the propertiesChanged signal of the device. Notice that
/org/bluez/hci0/dev_F8_E9_4E_A5_52_A1” is the path to the device, replace with what you have.

void BluetoothLE_AdvertSensor::initDBus()
{
    p_SensorDeviceInterface = new QDBusInterface ("org.bluez",
                                                  "/org/bluez/hci0/dev_F8_E9_4E_A5_52_A1",
                                                  "org.bluez.Device1",
                                                  QDBusConnection::systemBus());

    if(!p_SensorDeviceInterface->connection().connect("org.bluez",
                                         "/org/bluez/hci0/dev_F8_E9_4E_A5_52_A1",
                                         "org.freedesktop.DBus.Properties",
                                         "PropertiesChanged",
                                         this,
                                         SLOT(propertiesChanged(const QString, const QMap<QString,QVariant>, const QStringList)))) {
        qDebug() << "Failed to connect to D-Bus properties changed signal" << p_SensorDeviceInterface->lastError().name();
     }
}

And then look for the ManufacturerData

void BluetoothLE_AdvertSensor::propertiesChanged(const QString path, const QMap<QString, QVariant> changedProperties, const QStringList invalidatedProperties)
{
    Q_UNUSED(invalidatedProperties)
    Q_UNUSED(path)

    QMapIterator<QString, QVariant> mapIter(changedProperties);
    while(mapIter.hasNext()) {
        mapIter.next();

        // Advertised data is found in the ManufacturerData-property
        if(mapIter.key() == "ManufacturerData") {            
            QMap<int, QVariant> theData = qdbus_cast<QMap<int, QVariant> >(mapIter.value());
            QMapIterator<int, QVariant> dataIter(theData);

            while (dataIter.hasNext())
            {
                dataIter.next();
                // For this sensor, the interesting data appear if the key is 0x77 (Laird Company ID 1)
                if (dataIter.key() == 0x77)
                {
                    // Check the init-flag
                    if (!initSuccess)
                        setInitSuccess(true);
                    QByteArray arr = dataIter.value().toByteArray();                
                    qDebug() << QDateTime::currentDateTime() << " Length: " << arr.length() << " New values: " << arr;

                    auto data = reinterpret_cast<const quint8 *>(arr.constData());

                    // arr[4] and arr[5] == Flags, current state of alarms etc
                    quint16 newState = data[4] + (data[5] << 8);
                    this->setState(newState);

                    // ex        New values:  "\x01\x00\x00\x00\x07\x81\xD6\xAB<\xB6\x01\xDE\x04\x03\x02v\x02&a\x1F\t\x00\x00\x02"                    
                    // Data value part of 4 bytes, the LSB is data[19]
                    quint32 theValue = data[19] + (data[20] << 8) + (data[21] << 16) + (data[22] << 24);

                    // arr[12] == Type of data in theValue below
                    switch(data[12])
                    {

                    case 1:
                        // Temperature
                        qDebug() << "We have a new temperature of " << theValue/100.0f;
                        this->setTemperature(theValue);
                        break;
                    case 2:
                        // Magnet .. has a state
                        qDebug() << "Door is " << (theValue ? "open" : "closed");
                        break;
                    case 3:
                        // Movement
                        qDebug() << "We have movement!";
                        break;
                    case 4:
                        // High temp alarm
                        qDebug() << "We have a new high temperature of " << theValue/100.0f;
                        this->setTemperature(theValue);
                        break;
                    case 5:
                        // High temp 2 alarm
                        qDebug() << "We have a new high temperature 2 of " << theValue/100.0f;
                        this->setTemperature(theValue);
                        break;
                    case 6:
                        // Alarm high temp clear
                        qDebug() << "Alarm high temp clear at " << theValue/100.0f;
                        this->setTemperature(theValue);
                        break;
                    case 7:
                        // Alarm low temp 1
                        qDebug() << "Alarm low temp 1 at " << theValue/100.0f;
                        this->setTemperature(theValue);
                        break;
                    case 8:
                        // Alarm low temp 2
                        qDebug() << "Alarm low temp 2 at " << theValue/100.0f;
                        this->setTemperature(theValue);
                        break;
                    case 9:
                        // Alarm low temp clear
                        qDebug() << "Alarm low temp clear at " << theValue/100.0f;
                        this->setTemperature(theValue);
                        break;
                    case 10:
                        // Alarm delta temp
                        qDebug() << "Alarm delta temp " << theValue/100.0f;
                        this->setTemperature(theValue);
                        break;

                    case 12:
                        // Battery good
                        qDebug() << "Battery good at " << theValue << "mV";
                        this->setBattery(theValue);
                        break;
                    case 13:
                        // Advertise on button
                        qDebug() << "Advertise on button at " << theValue << "mV";
                        this->setBattery(theValue);
                        break;
                    case 16:
                        // Battery bad
                        qDebug() << "Battery bad at " << theValue << "mV";
                        this->setBattery(theValue);
                        break;
                    }
                }
            }
        }
        else {
            qDebug() << "Unknown property changed : " << mapIter.key();
        }
    }    
}

Implementing setters and adding signals for showing data in UI will be the easy part.

Reading data from heart rate sensor, Bluetooth LE

A heart sensor will implement the GATT service 0000180d-0000-1000-8000-00805f9b34fb (HRS - Heart rate service). This is how you usually use Bluetooth LE devices. They expose what services they support via GATT. If you find a device nearby supporting what you are looking for you try to connect to it.

You will use the gatt-api
to access the service and its characteristics.

With the help of GetManagedObjects (See link) you can check that your device has the org.bluez.GattService1 interface. You can also check that it has the org.bluez.GattCharacteristic1 interface for reading the different values exposed by the Heart rate service.

If you reference a characteristic you can read the UUID of the characteristic like below

QString HR_MEASUREMENT_CH_ID = "00002a37-0000-1000-8000-00805f9b34fb";
QDBusInterface* characteristic = new QDBusInterface ("org.bluez",
                                           aPath,
                                           "org.bluez.GattCharacteristic1",
                                           QDBusConnection::systemBus());
QString uuid = characteristic->property("UUID").toString();

if (uuid ==this->HR_MEASUREMENT_CH_ID)
    this->p_hrMeasurement = characteristic;

You also need a QDBusInterface to the device like seen before (dev_F8… should be replaced with your device)

p_heartrateDevice = new QDBusInterface ("org.bluez",
                                       "/org/bluez/hci0/dev_F8_E9_4E_A5_52_A1",
                                       "org.bluez.Device1",
                                       QDBusConnection::systemBus());

You should call Connect on the heartRateDevice and wait for the Connected property to be true.
To make the sensor start updating its heart rate value you need to call.

QDBusReply<void> reply = p_hrMeasurement->call("StartNotify");

General attribute variable transfer using Bluetooth LE

A Bluetooth LE device can be either peripheral or central. A peripheral device is often some sort of sensor and the central device is a more powerful device, usually a mobile phone or computer.

A peripheral device will start off by advertising what kind of services it provides. This communication is unidirectional. To communicate with peripheral BLE device a central device will connect to it. When a connection has been establised only bidirectional communication between the two devices is possible.

Services and characteristics

If we want a custom behaviour, not implementing a known service, we start by defining our own service and characteristics.

In the example below we define three variables exposed in our service.

const QBluetoothUuid ccReadVariableService = QBluetoothUuid(QString("cccccccc-cc16-4841-be7b-cb2d53dee717"));
const QBluetoothUuid ccReadVariableCharacteristicGauge1 = QBluetoothUuid(QString("cc02391c-53b5-4ead-b1b9-d3063407e90b"));
const QBluetoothUuid ccReadVariableCharacteristicGauge2 = QBluetoothUuid(QString("cc89ba3a-3b03-4e66-9560-14be899d607d"));
const QBluetoothUuid ccWriteVariableCharacteristicString = QBluetoothUuid(QString("cc0433cc-b100-45e7-bd91-2c2d3d3f907f"));

You can use a online tool for generating random UUID’s like we did above. An easy way for transferring data is to have one characteristic per value. A more advanced way would be to implement serial communication via one readable variable and one writeable.

Server part (the peripheral device)

Using some classes from QtBluetooth make this one easy.

  1. Setup the device and start advertising our custom service

  2. Handle new connection

  3. Update the Gauge1 (Readable) value as fast as possible in a loop

  4. Update the Gauge2 (Notify) value only when it changes

  5. Check if the client writes to the ccWriteVariableCharacteristicString value

  6. If the client (central device) disconnects, go back to advertising again.

The .h file for the server

#ifndef SERVER_H
#define SERVER_H

#include <QObject>
#include <QtBluetooth/qlowenergycontroller.h>
class server : public QObject
{
    Q_OBJECT

public:
    explicit server(QObject *parent = nullptr);
    Q_INVOKABLE void setGauge1Value(qreal value);
    Q_INVOKABLE void setGauge2Value(quint8 value);    
    Q_PROPERTY(QString info READ info NOTIFY infoChanged)
    Q_PROPERTY(QString clientMsg READ clientMsg NOTIFY clientMessageChanged)

    QString info() const;
    QString clientMsg() const;

private:
    void Setup();
    void Value1Limiter();
    void WriteValue1ToBT(qreal value);
    void setInfo(QString info);
    void setClientMsg(QString value);
    void characteristicWritten(const QLowEnergyCharacteristic &c,
                              const QByteArray &value);

    QLowEnergyController* leController;
    QLowEnergyService* leService;
    QLowEnergyAdvertisingData* leData;
    QLowEnergyCharacteristic leCharacteristic1;

    int m_value1MaxSendPerTimeout = 500; 
    int m_value1Counter = 0;
    bool m_value1Sleep = false;
    qreal m_value1LastSet;
    QString m_info;
    QString m_msgFromClient;

private slots:    
    void ControllerStateChanged(QLowEnergyController::ControllerState state);
signals:
    void infoChanged();
    void clientMessageChanged();
};

#endif // SERVER_H

Important bits in the cpp file. We setup our service with the three variables we defined UUIDs.

void server::Setup()
{
    //! [Advertising Data]
    leData = new  QLowEnergyAdvertisingData(); // advertisingData;
    leData->setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral);
    leData->setIncludePowerLevel(true);    
    leData->setServices(QList<QBluetoothUuid>() << ccReadVariableService);

    // Gauge1
    QLowEnergyCharacteristicData charData;
    charData.setUuid(ccReadVariableCharacteristicGauge1);
    // qreal for first value
    QByteArray arr1;
    arr1.setNum(qreal(0));
    charData.setValue(arr1);

    // Read on this one... you can pull a new value about 20 times per second
    charData.setProperties(QLowEnergyCharacteristic::Read);
    const QLowEnergyDescriptorData clientConfig(QBluetoothUuid::ClientCharacteristicConfiguration,
                                                QByteArray(2, 0));
    charData.addDescriptor(clientConfig);

    // Gauge 2, more of a state value in one single byte, use notify for this one
    QLowEnergyCharacteristicData charData2;
    charData2.setUuid(ccReadVariableCharacteristicGauge2);
    charData2.setValue(QByteArray(1, 1));
    charData2.setProperties(QLowEnergyCharacteristic::Notify);
    const QLowEnergyDescriptorData clientConfig2(QBluetoothUuid::ClientCharacteristicConfiguration,
                                                QByteArray(2, 0));
    charData2.addDescriptor(clientConfig2);

    // A short string to be set from client back to server (write)
    QLowEnergyCharacteristicData charData3;
    charData3.setUuid(ccWriteVariableCharacteristicString);
    QByteArray arr3(50,0);
    charData3.setValue(arr3);
    charData3.setValueLength(0,50);
    charData3.setProperties(QLowEnergyCharacteristic::Write);
    const QLowEnergyDescriptorData clientConfig3(QBluetoothUuid::ClientCharacteristicConfiguration,
                                                QByteArray(2, 0));
   charData3.addDescriptor(clientConfig3);

    // Setup the service, we are only using one service with three characteristics
    QLowEnergyServiceData serviceData;
    serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary);
    serviceData.setUuid(ccReadVariableService);
    serviceData.addCharacteristic(charData);
    serviceData.addCharacteristic(charData2);
    serviceData.addCharacteristic(charData3);

    //! [Start Advertising]
    leController = QLowEnergyController::createPeripheral();
    leService = leController->addService(serviceData);

    connect(leService, &QLowEnergyService::characteristicChanged, this, &server::characteristicWritten);

    leController->startAdvertising(QLowEnergyAdvertisingParameters(), *leData,
                                   *leData);
    // Keep track of state changes
    connect(leController, &QLowEnergyController::stateChanged,
            this, &server::ControllerStateChanged);

    connect(leController, static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
            this, [this](){
       qDebug() << "leController got error";
    });

    // Keep a reference to the first characteristic
    leCharacteristic1 = leService->characteristic(ccReadVariableCharacteristicGauge1);
}

To update the qreal-value (our first characteristic) we use this code

void server::WriteValue1ToBT(qreal value)
{
    QByteArray arr;
    arr.setNum(value);
    leService->writeCharacteristic(leCharacteristic1, arr);
}

Updating our second value, another way of writing without having the reference to the characteristic. This value is also setup as a Notify so it behaves like an event to the client side.

void server::setGauge2Value(quint8 value)
{
    QLowEnergyCharacteristic characteristic = leService->characteristic(ccReadVariableCharacteristicGauge2);
    QByteArray arr(1,value);
    leService->writeCharacteristic(characteristic, arr);
}

Our third variable is a string that is writeable from the client (so we don’t have any write-code on the server part) When the central device writes we can catch the value in:

void server::characteristicWritten(const QLowEnergyCharacteristic &c, const QByteArray &value)
{
    // Something was written from client side
    if (c.uuid() == ccWriteVariableCharacteristicString)
    {
        // Update msg from client
        qDebug() << "Client message " << QString::fromUtf8(value);
        setClientMsg(QString::fromUtf8(value));
    }

}

Also in the setup we catch when the state of the low energy adaptor changes.

void server::ControllerStateChanged(QLowEnergyController::ControllerState state)
{
    // State of leController changes
    qDebug() << "ControllerStateChanged to state " << state;
    
    // Restart advertising after a disconnect
    if (state != QLowEnergyController::AdvertisingState && state != QLowEnergyController::ConnectedState)
    {
        // Start advertisting again
        leController->startAdvertising(QLowEnergyAdvertisingParameters(), *leData,
                                       *leData);
    }
}

Client part (the central device)

As in other examples we can use the QBluetoothDeviceDiscoveryAgent class and call our function addDevice when devices are discovered.

m_deviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);
m_deviceDiscoveryAgent->setLowEnergyDiscoveryTimeout(5000);

connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &Discovery::addDevice);

When starting to scan we supply the LowEnergyMethod to the agent.

void Discovery::startSearch()
{
    // Find some Bluetooth LE devices

    m_deviceDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethod::LowEnergyMethod);
    emit scanningChanged();
}

We can filter out Bluetooth LE devices and check if the service uuid matches our custom ccReadVariableService. If we have a match we call StartConnect on our Client class described after this code block.

void Discovery::addDevice(const QBluetoothDeviceInfo &device)
{
    // If device is LowEnergy-device, add it to the list
    if (device.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration) {
        m_devices.append(new DeviceName(device));
        QList<QBluetoothUuid> services = device.serviceUuids();
        QBluetoothUuid theId;
        foreach(theId, services)
        {
            if (theId == ccReadVariableService)
            {
                m_client->setInfo("Found device " + device.name() );
                m_client->StartConnect(device);
            }
         }
     }
 }

A simple client class is created.

The .h file

#ifndef CLIENT_H
#define CLIENT_H

#include <QObject>
#include <QLowEnergyController>
#include <QLowEnergyService>
#include <QBluetoothLocalDevice>
#include "typedefs.h"

// Read data from a server using Bluetooth LE
class Client : public QObject
{
    Q_OBJECT
    Q_PROPERTY(qreal gauge1 READ gauge1 NOTIFY gauge1Changed)
    Q_PROPERTY(quint8 gauge2 READ gauge2 NOTIFY gauge2Changed)
    Q_PROPERTY(QString info READ info NOTIFY infoChanged)
    Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)

public:
    explicit Client(QObject *parent = nullptr);
    Q_INVOKABLE void boostConnection();
    Q_INVOKABLE void sayHiToServer();
    bool connected() const;
    qreal gauge1() const;
    quint8 gauge2() const;
    QString info() const;
    void StartConnect(const QBluetoothDeviceInfo &device);
     void setInfo(QString info);
signals:
    void gauge1Changed(); // The QML part will be interested
    void gauge2Changed(); // The QML part will be interested
    void infoChanged();
    void connectedChanged();

private:
    void setConnected(bool value);
    void setGauge1(qreal value);
    void setGauge2(quint8 value);
    void serviceDiscovered(const QBluetoothUuid &);
    void servicescanDone();
    void gotError(QLowEnergyController::Error err);
    void gotServiceError(QLowEnergyService::ServiceError err);
    void serviceStateChanged(QLowEnergyService::ServiceState s);
    void characteristicRead(const QLowEnergyCharacteristic &c,
                              const QByteArray &value);
    void updateDataValues(const QLowEnergyCharacteristic &c,
                              const QByteArray &value);

    void countTraffic();
    void pollData();

    qreal m_gauge1;
    quint8 m_gauge2;
    QString m_info;
    bool m_connected;
    bool m_pendingRead;

    QBluetoothLocalDevice m_localDevice;

    QLowEnergyController *m_control = nullptr;
    QLowEnergyService *m_service = nullptr;
    QLowEnergyDescriptor m_notificationDesc;

    int m_recieveCounter=0;
    int m_beginRecieveCounter=0;
    bool m_serviceDiscovered = false;
    int m_debugCounter = 0;
    int m_sayHiCounter = 0;
    int m_readTimeoutCounter = 0;
};

#endif // CLIENT_H

Important parts of cpp file

#include "client.h"
#include <QTimer>
#include <QLowEnergyConnectionParameters>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <qrandom.h>

Client::Client(QObject *parent) : QObject(parent)
{
    m_control = nullptr;
    m_gauge1 = 0;
    m_gauge2 = 0;
    m_info = "Client information";
    m_connected = false;
    m_pendingRead = false;
    m_localDevice.powerOn();
    m_localDevice.setHostMode(QBluetoothLocalDevice::HostDiscoverable);
    QTimer *timer = new QTimer(this);
    connect(timer, &QTimer::timeout, this, &Client::pollData);
    timer->start(10);

    QTimer *timer2 = new QTimer(this);
    connect(timer2, &QTimer::timeout, this, &Client::countTraffic);
    timer2->start(1000);

}

void Client::sayHiToServer()
{
    if (m_connected)
    {
        QStringList lst = {"Hi", "Hello","Godday","Morning","Whats up"};

        int choose = QRandomGenerator::global()->bounded(0, lst.length());
        QString str = QVariant(m_sayHiCounter++).toString() + " " + lst.at(choose);

        qDebug() << "Say hi to server: " << str;
        // Write string to server writeVariable
        QLowEnergyCharacteristic characteristic = m_service->characteristic(ccWriteVariableCharacteristicString);

        QByteArray arr = str.toUtf8();
        m_service->writeCharacteristic(characteristic, arr, QLowEnergyService::WriteWithoutResponse);
    }
}

qreal Client::gauge1() const
{
    return m_gauge1;
}

quint8 Client::gauge2() const
{
    return m_gauge2;
}

QString Client::info() const
{
    return m_info;
}

bool Client::connected() const
{
    return m_connected;
}

void Client::StartConnect(const QBluetoothDeviceInfo &device)
{
    // Init
    m_control = QLowEnergyController::createCentral(device);

    // Connect some interesting parts
    connect(m_control, &QLowEnergyController::discoveryFinished,
            this, &Client::servicescanDone);

    connect(m_control, &QLowEnergyController::serviceDiscovered,
            this, &Client::serviceDiscovered);

    connect(m_control, static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),
            this, &Client::gotError);

    connect(m_control, &QLowEnergyController::connected, this, [this]() {
        setInfo("Controller connected. Search services...");
        m_control->discoverServices();
    });

    connect(m_control, &QLowEnergyController::disconnected, this, [this]() {
        m_serviceDiscovered = false;
        setInfo("LowEnergy controller disconnected");
    });

    connect(m_control, &QLowEnergyController::connectionUpdated, this, [this](const QLowEnergyConnectionParameters para) {
        // Never gets called?
        qDebug() << "connectionUpdated: " << para.minimumInterval();
        setInfo("connectionUpdated");
    });

    // Try to connect
    qDebug() << "Connecting to device!!";
    m_control->connectToDevice();    
}

void Client::serviceDiscovered(const QBluetoothUuid &uuid)
{
    // Check order
    qDebug() << __PRETTY_FUNCTION__;
    qDebug() << uuid;

}

void Client::servicescanDone()
{
    qDebug() << __PRETTY_FUNCTION__;
    // Delete old service if available
    if (m_service) {
        delete m_service;
        m_service = nullptr;
    }
    m_service = m_control->createServiceObject(ccReadVariableService, this);
    if (m_service)
    {
        connect(m_service, &QLowEnergyService::characteristicChanged, this, &Client::updateDataValues);
        connect(m_service, &QLowEnergyService::stateChanged, this, &Client::serviceStateChanged);
        connect(m_service, &QLowEnergyService::characteristicRead, this, &Client::characteristicRead);
        connect(m_service, static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error), this, &Client::gotServiceError);
        m_service->discoverDetails();

    }
    else
    {
        setInfo("m_service null");
    }
}

void Client::gotError(QLowEnergyController::Error err)
{
    qDebug() << err;
    setInfo("Failed to connect");
}

void Client::gotServiceError(QLowEnergyService::ServiceError err)
{
    qDebug() << "Service error: " << err;
}

void Client::serviceStateChanged(QLowEnergyService::ServiceState s)
{
    if (s == QLowEnergyService::ServiceDiscovered)
    {
         m_serviceDiscovered = true;
         setConnected(true);        
        // Send notification start, needed for a characteristic with Notify
        const QLowEnergyCharacteristic hrChar = m_service->characteristic(ccReadVariableCharacteristicGauge2);
        m_notificationDesc = hrChar.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);

        if (m_notificationDesc.isValid())
            m_service->writeDescriptor(m_notificationDesc, QByteArray::fromHex("0100"));
        else
            setInfo("m_notificationDesc not valid");
    }
    else
    {
        setConnected(false);
    }
}

void Client::setGauge1(qreal value)
{
    if (value != m_gauge1)
    {
        m_gauge1 = value;
        emit gauge1Changed();
    }
}

void Client::setGauge2(quint8 value)
{
    if (value != m_gauge2)
    {
        m_gauge2 = value;
    }
}

void Client::characteristicRead(const QLowEnergyCharacteristic &c, const QByteArray &value)
{
    // I guess this is the answer from our polling of gauge1
    if (c.uuid() == ccReadVariableCharacteristicGauge1)
    {
        // Only gets called once when connected (a read variable), but also if request read
        setGauge1(value.toDouble());
        m_recieveCounter++;
        m_pendingRead = false;
    }
}

void Client::updateDataValues(const QLowEnergyCharacteristic &c, const QByteArray &value)
{

    // Depending on the type of c... update correct value    
    if (c.uuid() == ccReadVariableCharacteristicGauge2)
    {
        // Gets called everytime the value changes via notify, but not when first connected
        setGauge2((quint8)value[0]);
    }
}

void Client::countTraffic()
{
    // Timer calls this once a second
    if (m_recieveCounter > 0)
        qDebug() << "Recieve count " << m_recieveCounter << " begin count " << m_beginRecieveCounter;

    m_recieveCounter = 0;
    m_beginRecieveCounter = 0;

}

void Client::pollData()
{
    // if connected.. read data here
    if (m_serviceDiscovered)
    {        
        if (m_pendingRead == false || m_readTimeoutCounter > 100)
        {
            const QLowEnergyCharacteristic char1 = m_service->characteristic(ccReadVariableCharacteristicGauge1);
            m_service->readCharacteristic(char1);
            m_pendingRead = true;
            m_beginRecieveCounter++;
            m_readTimeoutCounter = 0;
        }
        else
            m_readTimeoutCounter++;
     }
}

void Client::setInfo(QString info)
{
    if (info != m_info)
    {
        m_info = info;
        emit infoChanged();
    }
}

void Client::setConnected(bool value)
{
    if (value != m_connected)
    {
        m_connected = value;
        emit connectedChanged();
    }
}

D-Bus abstraction with Qt

If you have an XML-file describing a D-Bus interface you can use a special tool called qdbusxml2cpp for generating a cpp class which contains Qt signal and slots for the interface.

Generating the XML can be done via the dbus-send tool using this syntax.

dbus-send --session --print-reply --dest=service.name /obj/path org.freedesktop.DBus.Introspectable.Introspect > name.xml

Bluetoothctl

Bluetoothctl is a command line tool to interact with the bluetoothd daemon and is very useful for debugging and testing different scenarios. You can access special menus for gatt, scan and advertise, which are used for Bluetooth LE.

Bluetoothctl

Scan menu

Scan menu

Gatt menu

Gatt menu

Obexctl

Obexctl is a command line tool for interacting with the obexd daemon which is used for sending/receiving files via Bluetooth.

Obexctl menu

Note: Obex is not supported on Apple products, at least not iPhone.